1   // Copyright 2011-2013 The Apache Software Foundation
2   //
3   // Licensed under the Apache License, Version 2.0 (the "License");
4   // you may not use this file except in compliance with the License.
5   // You may obtain a copy of the License at
6   //
7   // http://www.apache.org/licenses/LICENSE-2.0
8   //
9   // Unless required by applicable law or agreed to in writing, software
10  // distributed under the License is distributed on an "AS IS" BASIS,
11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  // See the License for the specific language governing permissions and
13  // limitations under the License.
14  
15  package org.apache.tapestry5.internal.plastic;
16  
17  import org.apache.tapestry5.internal.plastic.asm.ClassReader;
18  import org.apache.tapestry5.internal.plastic.asm.ClassWriter;
19  import org.apache.tapestry5.internal.plastic.asm.Opcodes;
20  import org.apache.tapestry5.internal.plastic.asm.tree.*;
21  import org.apache.tapestry5.plastic.*;
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  
25  import java.io.BufferedInputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.lang.annotation.Annotation;
29  import java.lang.reflect.Modifier;
30  import java.util.*;
31  import java.util.concurrent.CopyOnWriteArrayList;
32  
33  /**
34   * Responsible for managing a class loader that allows ASM {@link ClassNode}s
35   * to be instantiated as runtime classes.
36   */
37  @SuppressWarnings("rawtypes")
38  public class PlasticClassPool implements ClassLoaderDelegate, Opcodes, PlasticClassListenerHub
39  {
40      private static final Logger LOGGER = LoggerFactory.getLogger(PlasticClassPool.class);
41  
42      final PlasticClassLoader loader;
43  
44      private final PlasticManagerDelegate delegate;
45  
46      private final Set<String> controlledPackages;
47  
48      private final Map<String, Boolean> checkedExceptionCache = new HashMap<String, Boolean>();
49  
50  
51      // Would use Deque, but that's added in 1.6 and we're still striving for 1.5 code compatibility.
52  
53      private final Stack<String> activeInstrumentClassNames = new Stack<String>();
54  
55      /**
56       * Maps class names to instantiators for that class name.
57       * Synchronized on the loader.
58       */
59      private final Map<String, ClassInstantiator> instantiators = PlasticInternalUtils.newMap();
60  
61      private final InheritanceData emptyInheritanceData = new InheritanceData(null);
62  
63      private final StaticContext emptyStaticContext = new StaticContext();
64  
65      private final List<PlasticClassListener> listeners = new CopyOnWriteArrayList<PlasticClassListener>();
66  
67      private final Cache<String, TypeCategory> typeName2Category = new Cache<String, TypeCategory>()
68      {
69          @Override
70          protected TypeCategory convert(String typeName)
71          {
72              ClassNode cn = constructClassNodeFromBytecode(typeName);
73  
74              return Modifier.isInterface(cn.access) ? TypeCategory.INTERFACE : TypeCategory.CLASS;
75          }
76      };
77  
78      static class BaseClassDef
79      {
80          final InheritanceData inheritanceData;
81  
82          final StaticContext staticContext;
83  
84          public BaseClassDef(InheritanceData inheritanceData, StaticContext staticContext)
85          {
86              this.inheritanceData = inheritanceData;
87              this.staticContext = staticContext;
88          }
89      }
90  
91      /**
92       * Map from FQCN to BaseClassDef. Synchronized on the loader.
93       */
94      private final Map<String, BaseClassDef> baseClassDefs = PlasticInternalUtils.newMap();
95  
96  
97      private final Map<String, FieldInstrumentations> instrumentations = PlasticInternalUtils.newMap();
98  
99      private final Map<String, String> transformedClassNameToImplementationClassName = PlasticInternalUtils.newMap();
100 
101 
102     private final FieldInstrumentations placeholder = new FieldInstrumentations(null);
103 
104 
105     private final Set<TransformationOption> options;
106 
107     /**
108      * Creates the pool with a set of controlled packages; all classes in the controlled packages are loaded by the
109      * pool's class loader, and all top-level classes in the controlled packages are transformed via the delegate.
110      *
111      * @param parentLoader
112      *         typically, the Thread's context class loader
113      * @param delegate
114      *         responsible for end stages of transforming top-level classes
115      * @param controlledPackages
116      *         set of package names (note: retained, not copied)
117      * @param options
118      *         used when transforming classes
119      */
120     public PlasticClassPool(ClassLoader parentLoader, PlasticManagerDelegate delegate, Set<String> controlledPackages,
121                             Set<TransformationOption> options)
122     {
123         loader = new PlasticClassLoader(parentLoader, this);
124         this.delegate = delegate;
125         this.controlledPackages = controlledPackages;
126         this.options = options;
127     }
128 
129     public ClassLoader getClassLoader()
130     {
131         return loader;
132     }
133 
134     public Class realizeTransformedClass(ClassNode classNode, InheritanceData inheritanceData,
135                                          StaticContext staticContext)
136     {
137         synchronized (loader)
138         {
139             Class result = realize(PlasticInternalUtils.toClassName(classNode.name), ClassType.PRIMARY, classNode);
140             baseClassDefs.put(result.getName(), new BaseClassDef(inheritanceData, staticContext));
141 
142             return result;
143         }
144 
145     }
146 
147     public Class realize(String primaryClassName, ClassType classType, ClassNode classNode)
148     {
149         synchronized (loader)
150         {
151             if (!listeners.isEmpty())
152             {
153                 fire(toEvent(primaryClassName, classType, classNode));
154             }
155 
156             byte[] bytecode = toBytecode(classNode);
157 
158             String className = PlasticInternalUtils.toClassName(classNode.name);
159 
160             return loader.defineClassWithBytecode(className, bytecode);
161         }
162     }
163 
164     private PlasticClassEvent toEvent(final String primaryClassName, final ClassType classType,
165                                       final ClassNode classNode)
166     {
167         return new PlasticClassEvent()
168         {
169             @Override
170             public ClassType getType()
171             {
172                 return classType;
173             }
174 
175             @Override
176             public String getPrimaryClassName()
177             {
178                 return primaryClassName;
179             }
180 
181             @Override
182             public String getDissasembledBytecode()
183             {
184                 return PlasticInternalUtils.dissasembleBytecode(classNode);
185             }
186 
187             @Override
188             public String getClassName()
189             {
190                 return PlasticInternalUtils.toClassName(classNode.name);
191             }
192         };
193     }
194 
195     private void fire(PlasticClassEvent event)
196     {
197         for (PlasticClassListener listener : listeners)
198         {
199             listener.classWillLoad(event);
200         }
201     }
202 
203     private byte[] toBytecode(ClassNode classNode)
204     {
205         ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
206 
207         classNode.accept(writer);
208 
209         return writer.toByteArray();
210     }
211 
212     public AnnotationAccess createAnnotationAccess(String className)
213     {
214         try
215         {
216             final Class<?> searchClass = loader.loadClass(className);
217 
218             return new AnnotationAccess()
219             {
220                 @Override
221                 public <T extends Annotation> boolean hasAnnotation(Class<T> annotationType)
222                 {
223                     return getAnnotation(annotationType) != null;
224                 }
225 
226                 @Override
227                 public <T extends Annotation> T getAnnotation(Class<T> annotationType)
228                 {
229                     return searchClass.getAnnotation(annotationType);
230                 }
231             };
232         } catch (Exception ex)
233         {
234             throw new RuntimeException(ex);
235         }
236     }
237 
238     public AnnotationAccess createAnnotationAccess(List<AnnotationNode> annotationNodes)
239     {
240         if (annotationNodes == null)
241         {
242             return EmptyAnnotationAccess.SINGLETON;
243         }
244 
245         final Map<String, Object> cache = PlasticInternalUtils.newMap();
246         final Map<String, AnnotationNode> nameToNode = PlasticInternalUtils.newMap();
247 
248         for (AnnotationNode node : annotationNodes)
249         {
250             nameToNode.put(PlasticInternalUtils.objectDescriptorToClassName(node.desc), node);
251         }
252 
253         return new AnnotationAccess()
254         {
255             @Override
256             public <T extends Annotation> boolean hasAnnotation(Class<T> annotationType)
257             {
258                 return nameToNode.containsKey(annotationType.getName());
259             }
260 
261             @Override
262             public <T extends Annotation> T getAnnotation(Class<T> annotationType)
263             {
264                 String className = annotationType.getName();
265 
266                 Object result = cache.get(className);
267 
268                 if (result == null)
269                 {
270                     result = buildAnnotation(className);
271 
272                     if (result != null)
273                         cache.put(className, result);
274                 }
275 
276                 return annotationType.cast(result);
277             }
278 
279             private Object buildAnnotation(String className)
280             {
281                 AnnotationNode node = nameToNode.get(className);
282 
283                 if (node == null)
284                     return null;
285 
286                 return createAnnotation(className, node);
287             }
288         };
289     }
290 
291     Class loadClass(String className)
292     {
293         try
294         {
295             return loader.loadClass(className);
296         } catch (Exception ex)
297         {
298             throw new RuntimeException(String.format("Unable to load class %s: %s", className,
299                     PlasticInternalUtils.toMessage(ex)), ex);
300         }
301     }
302 
303     protected Object createAnnotation(String className, AnnotationNode node)
304     {
305         AnnotationBuilder builder = new AnnotationBuilder(loadClass(className), this);
306 
307         node.accept(builder);
308 
309         return builder.createAnnotation();
310     }
311 
312     @Override
313     public boolean shouldInterceptClassLoading(String className)
314     {
315         int searchFromIndex = className.length() - 1;
316 
317         while (true)
318         {
319             int dotx = className.lastIndexOf('.', searchFromIndex);
320 
321             if (dotx < 0)
322                 break;
323 
324             String packageName = className.substring(0, dotx);
325 
326             if (controlledPackages.contains(packageName))
327                 return true;
328 
329             searchFromIndex = dotx - 1;
330         }
331 
332         return false;
333     }
334 
335     // Hopefully the synchronized will not cause a deadlock
336 
337     @Override
338     public synchronized Class<?> loadAndTransformClass(String className) throws ClassNotFoundException
339     {
340         // Inner classes are not transformed, but they are loaded by the same class loader.
341 
342         if (className.contains("$"))
343         {
344             return loadInnerClass(className);
345         }
346 
347         // TODO: What about interfaces, enums, annotations, etc. ... they shouldn't be in the package, but
348         // we should generate a reasonable error message.
349 
350         if (activeInstrumentClassNames.contains(className))
351         {
352             StringBuilder builder = new StringBuilder("");
353             String sep = "";
354 
355             for (String name : activeInstrumentClassNames)
356             {
357                 builder.append(sep);
358                 builder.append(name);
359 
360                 sep = ", ";
361             }
362 
363             throw new IllegalStateException(String.format("Unable to transform class %s as it is already in the process of being transformed; there is a cycle among the following classes: %s.",
364                     className, builder));
365         }
366 
367         activeInstrumentClassNames.push(className);
368 
369         try
370         {
371 
372             InternalPlasticClassTransformation transformation = getPlasticClassTransformation(className);
373 
374             delegate.transform(transformation.getPlasticClass());
375 
376             ClassInstantiator createInstantiator = transformation.createInstantiator();
377             ClassInstantiator configuredInstantiator = delegate.configureInstantiator(className, createInstantiator);
378 
379             instantiators.put(className, configuredInstantiator);
380 
381             return transformation.getTransformedClass();
382         } finally
383         {
384             activeInstrumentClassNames.pop();
385         }
386     }
387 
388     private Class loadInnerClass(String className)
389     {
390         ClassNode classNode = constructClassNodeFromBytecode(className);
391 
392         interceptFieldAccess(classNode);
393 
394         return realize(className, ClassType.INNER, classNode);
395     }
396 
397     private void interceptFieldAccess(ClassNode classNode)
398     {
399         for (MethodNode method : classNode.methods)
400         {
401             interceptFieldAccess(classNode.name, method);
402         }
403     }
404 
405     private void interceptFieldAccess(String classInternalName, MethodNode method)
406     {
407         InsnList insns = method.instructions;
408 
409         ListIterator it = insns.iterator();
410 
411         while (it.hasNext())
412         {
413             AbstractInsnNode node = (AbstractInsnNode) it.next();
414 
415             int opcode = node.getOpcode();
416 
417             if (opcode != GETFIELD && opcode != PUTFIELD)
418             {
419                 continue;
420             }
421 
422             FieldInsnNode fnode = (FieldInsnNode) node;
423 
424             String ownerInternalName = fnode.owner;
425 
426             if (ownerInternalName.equals(classInternalName))
427             {
428                 continue;
429             }
430 
431             FieldInstrumentation instrumentation = getFieldInstrumentation(ownerInternalName, fnode.name, opcode == GETFIELD);
432 
433             if (instrumentation == null)
434             {
435                 continue;
436             }
437 
438             // Replace the field access node with the appropriate method invocation.
439 
440             insns.insertBefore(fnode, new MethodInsnNode(INVOKEVIRTUAL, ownerInternalName, instrumentation.methodName, instrumentation.methodDescription));
441 
442             it.remove();
443         }
444     }
445 
446 
447     /**
448      * For a fully-qualified class name of an <em>existing</em> class, loads the bytes for the class
449      * and returns a PlasticClass instance.
450      *
451      * @throws ClassNotFoundException
452      */
453     public InternalPlasticClassTransformation getPlasticClassTransformation(String className)
454             throws ClassNotFoundException
455     {
456         assert PlasticInternalUtils.isNonBlank(className);
457 
458         ClassNode classNode = constructClassNodeFromBytecode(className);
459 
460         String baseClassName = PlasticInternalUtils.toClassName(classNode.superName);
461 
462         instrumentations.put(classNode.name, new FieldInstrumentations(classNode.superName));
463 
464         // TODO: check whether second parameter should really be null
465         return createTransformation(baseClassName, classNode, null, false);
466     }
467 
468     /**
469      * @param baseClassName
470      *         class from which the transformed class extends
471      * @param classNode
472      *         node for the class
473      * @param implementationClassNode
474      *         node for the implementation class. May be null.
475      * @param proxy
476      *         if true, the class is a new empty class; if false an existing class that's being transformed
477      * @throws ClassNotFoundException
478      */
479     private InternalPlasticClassTransformation createTransformation(String baseClassName, ClassNode classNode, ClassNode implementationClassNode, boolean proxy)
480             throws ClassNotFoundException
481     {
482         if (shouldInterceptClassLoading(baseClassName))
483         {
484             loader.loadClass(baseClassName);
485 
486             BaseClassDef def = baseClassDefs.get(baseClassName);
487 
488             assert def != null;
489 
490             return new PlasticClassImpl(classNode, implementationClassNode, this, def.inheritanceData, def.staticContext, proxy);
491         }
492 
493         // When the base class is Object, or otherwise not in a transformed package,
494         // then start with the empty
495         return new PlasticClassImpl(classNode, implementationClassNode, this, emptyInheritanceData, emptyStaticContext, proxy);
496     }
497 
498     /**
499      * Constructs a class node by reading the raw bytecode for a class and instantiating a ClassNode
500      * (via {@link ClassReader#accept(org.apache.tapestry5.internal.plastic.asm.ClassVisitor, int)}).
501      *
502      * @param className
503      *         fully qualified class name
504      * @return corresponding ClassNode
505      */
506     public ClassNode constructClassNodeFromBytecode(String className)
507     {
508         byte[] bytecode = readBytecode(className);
509 
510         if (bytecode == null)
511             return null;
512 
513         return PlasticInternalUtils.convertBytecodeToClassNode(bytecode);
514     }
515 
516     private byte[] readBytecode(String className)
517     {
518         ClassLoader parentClassLoader = loader.getParent();
519 
520         return PlasticInternalUtils.readBytecodeForClass(parentClassLoader, className, true);
521     }
522 
523     public PlasticClassTransformation createTransformation(String baseClassName, String newClassName)
524     {
525         return createTransformation(baseClassName, newClassName, null);
526     }
527 
528     public PlasticClassTransformation createTransformation(String baseClassName, String newClassName, String implementationClassName)
529     {
530         try
531         {
532             ClassNode newClassNode = new ClassNode();
533 
534             final String internalNewClassNameinternalName = PlasticInternalUtils.toInternalName(newClassName);
535             final String internalBaseClassName = PlasticInternalUtils.toInternalName(baseClassName);
536             newClassNode.visit(PlasticConstants.DEFAULT_VERSION_OPCODE, ACC_PUBLIC, internalNewClassNameinternalName, null, internalBaseClassName, null);
537 
538             ClassNode implementationClassNode = null;
539 
540             if (implementationClassName != null)
541             {
542                 // When decorating or advising a service, implementationClassName is the name
543                 // of a proxy class already, such as "$ServiceName_[random string]",
544                 // which doesn't exist as a file in the classpath, just in memory.
545                 // So we need to keep what's the original implementation class name
546                 // for each proxy, even a proxy around a proxy.
547                 if (transformedClassNameToImplementationClassName.containsKey(implementationClassName))
548                 {
549                     implementationClassName =
550                             transformedClassNameToImplementationClassName.get(implementationClassName);
551                 }
552 
553                 if (!implementationClassName.startsWith("com.sun.proxy"))
554                 {
555 
556                     try
557                     {
558                         implementationClassNode = readClassNode(implementationClassName);
559                     } catch (IOException e)
560                     {
561                         LOGGER.warn(String.format("Unable to load class %s as the implementation of service %s",
562                                 implementationClassName, baseClassName));
563                         // Go on. Probably a proxy class
564                     }
565 
566                 }
567 
568                 transformedClassNameToImplementationClassName.put(newClassName, implementationClassName);
569 
570             }
571 
572             return createTransformation(baseClassName, newClassNode, implementationClassNode, true);
573         } catch (ClassNotFoundException ex)
574         {
575             throw new RuntimeException(String.format("Unable to create class %s as sub-class of %s: %s", newClassName,
576                     baseClassName, PlasticInternalUtils.toMessage(ex)), ex);
577         }
578 
579     }
580 
581     private ClassNode readClassNode(String className) throws IOException
582     {
583         return readClassNode(className, getClassLoader());
584     }
585 
586     static ClassNode readClassNode(String className, ClassLoader classLoader) throws IOException
587     {
588         ClassNode classNode = new ClassNode();
589         final String location = PlasticInternalUtils.toInternalName(className) + ".class";
590         InputStream inputStream = classLoader.getResourceAsStream(location);
591         BufferedInputStream bis = new BufferedInputStream(inputStream);
592         ClassReader classReader = new ClassReader(inputStream);
593         inputStream.close();
594         bis.close();
595         classReader.accept(classNode, 0);
596         return classNode;
597 
598     }
599 
600     public ClassInstantiator getClassInstantiator(String className)
601     {
602         synchronized (loader)
603         {
604             if (!instantiators.containsKey(className))
605             {
606                 try
607                 {
608                     loader.loadClass(className);
609                 } catch (ClassNotFoundException ex)
610                 {
611                     throw new RuntimeException(ex);
612                 }
613             }
614 
615             ClassInstantiator result = instantiators.get(className);
616 
617             if (result == null)
618             {
619                 // TODO: Verify that the problem is incorrect package, and not any other failure.
620 
621                 StringBuilder b = new StringBuilder();
622                 b.append("Class '")
623                         .append(className)
624                         .append("' is not a transformed class. Transformed classes should be in one of the following packages: ");
625 
626                 String sep = "";
627 
628                 List<String> names = new ArrayList<String>(controlledPackages);
629                 Collections.sort(names);
630 
631                 for (String name : names)
632                 {
633                     b.append(sep);
634                     b.append(name);
635 
636                     sep = ", ";
637                 }
638 
639                 String message = b.append('.').toString();
640 
641                 throw new IllegalArgumentException(message);
642             }
643 
644             return result;
645         }
646     }
647 
648     TypeCategory getTypeCategory(String typeName)
649     {
650         synchronized (loader)
651         {
652             // TODO: Is this the right place to cache this data?
653 
654             return typeName2Category.get(typeName);
655         }
656     }
657 
658     @Override
659     public void addPlasticClassListener(PlasticClassListener listener)
660     {
661         assert listener != null;
662 
663         listeners.add(listener);
664     }
665 
666     @Override
667     public void removePlasticClassListener(PlasticClassListener listener)
668     {
669         assert listener != null;
670 
671         listeners.remove(listener);
672     }
673 
674     boolean isEnabled(TransformationOption option)
675     {
676         return options.contains(option);
677     }
678 
679 
680     void setFieldReadInstrumentation(String classInternalName, String fieldName, FieldInstrumentation fi)
681     {
682         instrumentations.get(classInternalName).read.put(fieldName, fi);
683     }
684 
685 
686     private FieldInstrumentations getFieldInstrumentations(String classInternalName)
687     {
688         FieldInstrumentations result = instrumentations.get(classInternalName);
689 
690         if (result != null)
691         {
692             return result;
693         }
694 
695         String className = PlasticInternalUtils.toClassName(classInternalName);
696 
697         // If it is a top-level (not inner) class in a controlled package, then we
698         // will recursively load the class, to identify any field instrumentations
699         // in it.
700         if (!className.contains("$") && shouldInterceptClassLoading(className))
701         {
702             try
703             {
704                 loadAndTransformClass(className);
705 
706                 // The key is written into the instrumentations map as a side-effect
707                 // of loading the class.
708                 return instrumentations.get(classInternalName);
709             } catch (Exception ex)
710             {
711                 throw new RuntimeException(PlasticInternalUtils.toMessage(ex), ex);
712             }
713         }
714 
715         // Either a class outside of controlled packages, or an inner class. Use a placeholder
716         // that contains empty maps.
717 
718         result = placeholder;
719         instrumentations.put(classInternalName, result);
720 
721         return result;
722     }
723 
724     FieldInstrumentation getFieldInstrumentation(String ownerClassInternalName, String fieldName, boolean forRead)
725     {
726         String currentName = ownerClassInternalName;
727 
728         while (true)
729         {
730 
731             if (currentName == null)
732             {
733                 return null;
734             }
735 
736             FieldInstrumentations instrumentations = getFieldInstrumentations(currentName);
737 
738             FieldInstrumentation instrumentation = instrumentations.get(fieldName, forRead);
739 
740             if (instrumentation != null)
741             {
742                 return instrumentation;
743             }
744 
745             currentName = instrumentations.superClassInternalName;
746         }
747     }
748 
749 
750     void setFieldWriteInstrumentation(String classInternalName, String fieldName, FieldInstrumentation fi)
751     {
752         instrumentations.get(classInternalName).write.put(fieldName, fi);
753     }
754 
755     boolean isCheckedException(String exceptionName)
756     {
757         Boolean cached = checkedExceptionCache.get(exceptionName);
758 
759         if (cached != null)
760         {
761             return cached;
762         }
763 
764         try
765         {
766             Class asClass = getClassLoader().loadClass(exceptionName);
767 
768 
769             boolean checked = !(Error.class.isAssignableFrom(asClass) ||
770                     RuntimeException.class.isAssignableFrom(asClass));
771 
772             checkedExceptionCache.put(exceptionName, checked);
773 
774             return checked;
775         } catch (Exception e)
776         {
777             throw new RuntimeException(e);
778         }
779     }
780 }
781 
782